﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

namespace TypeBuilderFx.Core
{
    /// <summary>Extends a <see cref="TypeBuilder"/> with higher-level methods.</summary>
    public class TypeBuilderExtender
    {
        //	Method attributes for methods hidden by an interface implementation.
        private const MethodAttributes HIDDEN_METHOD_ATTRIBUTES =
            MethodAttributes.Private
            | MethodAttributes.HideBySig
            | MethodAttributes.NewSlot
            | MethodAttributes.Virtual
            | MethodAttributes.Final;
        //	Method attributes for get & set methods hidden by an interface implementation.
        private const MethodAttributes HIDDEN_METHOD_GET_SET_ATTRIBUTES =
            HIDDEN_METHOD_ATTRIBUTES
            | MethodAttributes.SpecialName;
        //	Method attributes for add & remove methods hidden by an interface implementation.
        private const MethodAttributes HIDDEN_METHOD_ADD_REMOVE_ATTRIBUTES =
            HIDDEN_METHOD_ATTRIBUTES
            | MethodAttributes.SpecialName;

        private readonly TypeBuilder _typeBuilder;
        private readonly IDictionary<string, FieldBuilder> _fieldDictionary = new Dictionary<string, FieldBuilder>();

        /// <summary>Construct an extender by wrapping a <see cref="TypeBuilder"/>.</summary>
        /// <param name="typeBuilder"></param>
        public TypeBuilderExtender(TypeBuilder typeBuilder)
        {
            _typeBuilder = typeBuilder;
        }

        /// <summary>Exposes the underlying <see cref="TypeBuilder"/>.</summary>
        public TypeBuilder TypeBuilder
        {
            get { return _typeBuilder; }
        }

        /// <summary>Defines a method hidden by an interface.</summary>
        /// <param name="methodInfo">Method info on the owning interface.</param>
        /// <returns></returns>
        public ILGeneratorExtender DefineHiddenMethod(MethodInfo methodInfo)
        {
            if (methodInfo == null)
            {
                throw new ArgumentNullException("methodInfo");
            }

            Type interfaceType = methodInfo.DeclaringType;

            if (!interfaceType.IsInterface)
            {
                throw new ArgumentException("Should belong to an interface, not a concrete type", "methodInfo");
            }

            Type[] parameterTypeList = Array.ConvertAll(
                methodInfo.GetParameters(),
                delegate(ParameterInfo parameter) { return parameter.ParameterType; });
            MethodBuilder methodBuilder = TypeBuilder.DefineMethod(
                string.Concat(interfaceType.FullName, ".", methodInfo.Name),
                HIDDEN_METHOD_ATTRIBUTES,
                methodInfo.ReturnType,
                parameterTypeList);

            TypeBuilder.DefineMethodOverride(methodBuilder, methodInfo);

            return new ILGeneratorExtender(methodBuilder.GetILGenerator(), _fieldDictionary);
        }

        /// <summary>
        /// Defines a property hidden by an interface and returns its get and set methods (or <c>null</c> if not defined
        /// on the property.
        /// </summary>
        /// <param name="propertyInfo"></param>
        /// <param name="ilGeneratorGet"></param>
        /// <param name="ilGeneratorSet"></param>
        public void DefineHiddenProperty(
            PropertyInfo propertyInfo,
            out ILGeneratorExtender ilGeneratorGet,
            out ILGeneratorExtender ilGeneratorSet)
        {
            if (propertyInfo == null)
            {
                throw new ArgumentNullException("propertyInfo");
            }

            Type interfaceType = propertyInfo.DeclaringType;

            if (!interfaceType.IsInterface)
            {
                throw new ArgumentException("Should belong to an interface, not a concrete type", "propertyInfo");
            }

            PropertyBuilder propertyBuilder = TypeBuilder.DefineProperty(
                string.Concat(interfaceType.FullName, ".", propertyInfo.Name),
                PropertyAttributes.HasDefault,
                propertyInfo.PropertyType,
                null);

            if (propertyInfo.CanRead)
            {
                MethodInfo getMethodInfo = propertyInfo.GetGetMethod();
                MethodBuilder methodBuilder = TypeBuilder.DefineMethod(
                    string.Concat(interfaceType.FullName, ".", getMethodInfo.Name),
                    HIDDEN_METHOD_GET_SET_ATTRIBUTES,
                    getMethodInfo.ReturnType,
                    Type.EmptyTypes);

                ilGeneratorGet =
                    new ILGeneratorExtender(methodBuilder.GetILGenerator(), _fieldDictionary);
                propertyBuilder.SetGetMethod(methodBuilder);
                TypeBuilder.DefineMethodOverride(methodBuilder, getMethodInfo);
            }
            else
            {
                ilGeneratorGet = null;
            }
            if (propertyInfo.CanWrite)
            {
                MethodInfo setMethodInfo = propertyInfo.GetSetMethod();
                MethodBuilder methodBuilder = TypeBuilder.DefineMethod(
                    string.Concat(interfaceType.FullName, ".", setMethodInfo.Name),
                    HIDDEN_METHOD_GET_SET_ATTRIBUTES,
                    setMethodInfo.ReturnType,
                    new Type[] { propertyInfo.PropertyType });

                ilGeneratorSet = new ILGeneratorExtender(methodBuilder.GetILGenerator(), _fieldDictionary);
                propertyBuilder.SetSetMethod(methodBuilder);
                TypeBuilder.DefineMethodOverride(methodBuilder, setMethodInfo);
            }
            else
            {
                ilGeneratorSet = null;
            }
        }

        /// <summary>Defines an event hidden by an interface and returns its add and remove methods.</summary>
        /// <param name="eventInfo"></param>
        /// <param name="ilGeneratorAdd"></param>
        /// <param name="ilGeneratorRemove"></param>
        public void DefineHiddenEvent(
            EventInfo eventInfo, out ILGeneratorExtender ilGeneratorAdd, out ILGeneratorExtender ilGeneratorRemove)
        {
            if (eventInfo == null)
            {
                throw new ArgumentNullException("eventInfo");
            }

            Type interfaceType = eventInfo.DeclaringType;

            if (!interfaceType.IsInterface)
            {
                throw new ArgumentException("Should belong to an interface, not a concrete type", "eventInfo");
            }

            EventBuilder eventBuilder = TypeBuilder.DefineEvent(
                string.Concat(interfaceType.FullName, ".", eventInfo.Name),
                eventInfo.Attributes,
                eventInfo.EventHandlerType);
            MethodInfo addMethodInfo = eventInfo.GetAddMethod();
            MethodInfo removeMethodInfo = eventInfo.GetRemoveMethod();
            MethodBuilder addMethodBuilder = TypeBuilder.DefineMethod(
                string.Concat(interfaceType.FullName, ".", addMethodInfo.Name),
                HIDDEN_METHOD_ADD_REMOVE_ATTRIBUTES,
                null,
                new Type[] { eventInfo.EventHandlerType });
            MethodBuilder removeMethodBuilder = TypeBuilder.DefineMethod(
                string.Concat(interfaceType.FullName, ".", removeMethodInfo.Name),
                HIDDEN_METHOD_ADD_REMOVE_ATTRIBUTES,
                null,
                new Type[] { eventInfo.EventHandlerType });

            ilGeneratorAdd =
                new ILGeneratorExtender(addMethodBuilder.GetILGenerator(), _fieldDictionary);
            ilGeneratorRemove =
                new ILGeneratorExtender(removeMethodBuilder.GetILGenerator(), _fieldDictionary);
            eventBuilder.SetAddOnMethod(addMethodBuilder);
            eventBuilder.SetRemoveOnMethod(removeMethodBuilder);
            TypeBuilder.DefineMethodOverride(addMethodBuilder, addMethodInfo);
            TypeBuilder.DefineMethodOverride(removeMethodBuilder, removeMethodInfo);
        }

        /// <summary>Defines a static constructor.</summary>
        /// <returns></returns>
        public ILGeneratorExtender DefineStaticConstructor()
        {
            MethodAttributes attributes = MethodAttributes.HideBySig
                | MethodAttributes.Private
                | MethodAttributes.Static
                | MethodAttributes.SpecialName
                | MethodAttributes.RTSpecialName;
            ConstructorBuilder constructorBuilder = TypeBuilder.DefineConstructor(
                attributes,
                CallingConventions.Standard,
                Type.EmptyTypes);

            ILGeneratorExtender ilGenerator = new ILGeneratorExtender(constructorBuilder.GetILGenerator(), _fieldDictionary);

            return ilGenerator;
        }

        /// <summary>Defines a public constructor.</summary>
        /// <param name="parameterTypeList"></param>
        /// <returns></returns>
        public ILGeneratorExtender DefinePublicInstanceConstructor(params MethodParameter[] parameterList)
        {
            Type[] parameterTypeList = Array.ConvertAll(parameterList, delegate(MethodParameter parameter)
            {
                return parameter.Type;
            });
            ConstructorBuilder constructorBuilder = TypeBuilder.DefineConstructor(
                MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
                CallingConventions.Standard,
                parameterTypeList);

            for (int i = 0; i != parameterList.Length; ++i)
            {
                constructorBuilder.DefineParameter(i + 1, parameterList[i].Attributes, parameterList[i].Name);
            }

            ILGeneratorExtender ilGenerator = new ILGeneratorExtender(constructorBuilder.GetILGenerator(), _fieldDictionary);

            return ilGenerator;
        }

        /// <summary>Defines a private field.</summary>
        /// <param name="fieldName"/>
        /// <param name="fieldType"/>
        /// <param name="isReadOnly"/>
        /// <param name="isStatic"/>
        /// <returns></returns>
        public FieldBuilder DefinePrivateField(string fieldName, Type fieldType, bool isReadOnly, bool isStatic)
        {
            FieldAttributes fieldAttributes = FieldAttributes.Private;

            if (isReadOnly)
            {
                fieldAttributes |= FieldAttributes.InitOnly;
            }
            if (isStatic)
            {
                fieldAttributes |= FieldAttributes.Static;
            }

            FieldBuilder fieldBuilder = TypeBuilder.DefineField(fieldName, fieldType, fieldAttributes);

            _fieldDictionary.Add(fieldName, fieldBuilder);

            return fieldBuilder;
        }
    }
}